﻿#region NeoPatterns
// 
// NeoPatterns framework is released under LGPLv3 license.
// See http://www.opensource.org/licenses/lgpl-3.0.html for more details.
// 
// Please visit http://neopatterns.wikidot.com for more information about NeoPatterns
// 
// Copyright (c) Pascal Craponne 2008
// 
#endregion

using System;
using System.Collections.Generic;
using NeoPatterns.Exception;

namespace NeoPatterns.Patterns.Cache
{
    /// <summary>
    /// Cache implementation with maximum capacity and time expiration
    /// </summary>
    /// <typeparam name="TKey"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    public class MemoryCache<TKey, TValue> : ICache<TKey, TValue>
    {
        protected class ValueHolder
        {
            public DateTime ExpirationTime;
            public long Serial;
            public TKey Key;
            public TValue Value;
        }

        private int capacity;
        /// <summary>
        /// Cache capacity. Leave 0 for unlimited
        /// </summary>
        public int Capacity
        {
            get { return capacity; }
            set
            {
                if (capacity == value)
                    return;
                if (table.Count != 0)
                    throw new ConsistencyException();
                capacity = value;
                table = new Dictionary<TKey, ValueHolder>(capacity);
                ru = new SortedList<long, TKey>(capacity);
            }
        }

        private TimeSpan expiration;

        /// <summary>
        /// Item expiration, in seconds. Leave 0 for no expiration
        /// </summary>
        public double Expiration
        {
            get { return expiration.TotalSeconds; }
            set { expiration = TimeSpan.FromSeconds(value); }
        }

        /// <summary>
        /// Item sliding expiration. If true, the fact of getting the item out of cache will reset its time to live
        /// </summary>
        public bool SlidingExpiration { get; set; }

        /// <summary>
        /// Returns current DateTime,
        /// </summary>
        public Func<DateTime> Now = () => DateTime.Now;

        protected bool UseRU { get { return ru != null; } }
        protected bool UseExpiration { get { return Expiration > 0; } }

        private IDictionary<TKey, ValueHolder> table = new Dictionary<TKey, ValueHolder>();
        /// <summary>
        /// Use table: keeps track (based on an incremental index) of all objects in cache
        /// </summary>
        private SortedList<long, TKey> ru;
        /// <summary>
        /// Current serial index
        /// </summary>
        private long serial;

        /// <summary>
        /// Tries to get the value from cache (if available)
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool TryGet(TKey key, out TValue value)
        {
            ValueHolder valueHolder;
            // lookup for value
            if (table.TryGetValue(key, out valueHolder))
            {
                // and checks if it hasn't expired
                if (!HasExpired(valueHolder))
                {
                    if (SlidingExpiration)
                        Touch(valueHolder);
                    value = valueHolder.Value;
                    return true;
                }
                // it has expired, we remove it here
                Remove(valueHolder);
            }
            value = default(TValue);
            return false;
        }

        /// <summary>
        /// Adds value to the cache
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Set(TKey key, TValue value)
        {
            // if it exists, remove the old value
            ValueHolder valueHolder;
            if (table.TryGetValue(key, out valueHolder))
                Remove(valueHolder);
            // then create and add
            valueHolder = new ValueHolder { Value = value, Key = key };
            Add(valueHolder);
            CheckOverflow();
        }

        /// <summary>
        /// Checks if the maximum item count is reached
        /// </summary>
        protected virtual void CheckOverflow()
        {
            if (UseRU)
            {
                while (table.Count > Capacity)
                    PurgeLRU();
            }
        }

        /// <summary>
        /// Removes LRU entry
        /// </summary>
        protected virtual void PurgeLRU()
        {
            var key = ru[ru.Keys[0]];
            Remove(table[key]);
        }

        /// <summary>
        /// Adds a given ValueHolder to cache
        /// </summary>
        /// <param name="valueHolder"></param>
        protected virtual void Add(ValueHolder valueHolder)
        {
            // MRU uses serials
            if (UseRU)
            {
                valueHolder.Serial = ++serial;
                ru[valueHolder.Serial] = valueHolder.Key;
            }
            if (UseExpiration)
            {
                valueHolder.ExpirationTime = Now() + expiration;
            }
            table[valueHolder.Key] = valueHolder;
        }

        /// <summary>
        /// Checks if the given value has expired
        /// </summary>
        /// <param name="valueHolder"></param>
        /// <returns></returns>
        protected virtual bool HasExpired(ValueHolder valueHolder)
        {
            if (!UseExpiration)
                return false;
            return Now() >= valueHolder.ExpirationTime;
        }

        /// <summary>
        /// Removes an object from the cache
        /// </summary>
        /// <param name="valueHolder"></param>
        protected virtual void Remove(ValueHolder valueHolder)
        {
            if (UseRU)
            {
                ru.Remove(valueHolder.Serial);
            }
            table.Remove(valueHolder.Key);
        }

        /// <summary>
        /// Touches an object in the cache, ie refreshes its expiration
        /// </summary>
        /// <param name="valueHolder"></param>
        protected virtual void Touch(ValueHolder valueHolder)
        {
            Remove(valueHolder);
            Add(valueHolder);
        }

        public MemoryCache()
        {
        }

        public MemoryCache(int capacity)
        {
            Capacity = capacity;
        }
        public MemoryCache(TimeSpan expiration, bool slidingExpiration)
        {
            this.expiration = expiration;
            SlidingExpiration = slidingExpiration;
        }
        public MemoryCache(TimeSpan expiration, bool slidingExpiration, int capacity)
        {
            this.expiration = expiration;
            SlidingExpiration = slidingExpiration;
            Capacity = capacity;
        }
    }
}